Skip to content

Fix steam.exe crash on driver unload (0xc0000005) - fixes #3205#3333

Merged
zmerp merged 2 commits into
alvr-org:masterfrom
besauce:steam-probe-unload-crash
Jul 8, 2026
Merged

Fix steam.exe crash on driver unload (0xc0000005) - fixes #3205#3333
zmerp merged 2 commits into
alvr-org:masterfrom
besauce:steam-probe-unload-crash

Conversation

@besauce

@besauce besauce commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

steam.exe crashes with an access violation (0xc0000005) in driver_alvr_server.dll_unloaded. Reproduces reliably here: start SteamVR through Steam, then quit. steam.exe crashes on exit.

Steam enumerates VR drivers by loading the DLL, calling HmdDriverFactory, then unloading it with FreeLibrary. The factory currently runs the whole driver init on first call, so a simple probe spawns threads, shutdown_driver() never runs in that process, and the threads are still alive when the DLL gets unmapped. They end up executing freed memory.

WinDbg on the minidumps with local PDBs showed which threads were dying: first a rust thread mid teardown (thread_start into __rust_dealloc), then rayon workers (WorkerThread::find_work, Sleep::sleep). The rayon pool comes from sysinfo::System::new_all() in the factory, sysinfo's default multithread feature spins it up and it's never joined.

The fix: heavy init moved out of the factory into a new initialize_runtime() called from DriverProvider::Init(), so a probe spawns nothing and init only happens when vrserver actually activates the driver. shutdown_driver() now joins the event loop and idle init threads. And sysinfo's multithread feature is disabled in server_openvr and server_core to drop the rayon pool.

Verified on v20.14.1, the crash hit every SteamVR session before and is gone after. This branch is the same fix rebased onto master and it compiles.

Same crash as #3331 but fixed by removing the cause instead of special casing steam.exe. Valve's own driver_vrlink.dll crashes the same way in steam.exe, which fits the probe and unload mechanism.

@zmerp zmerp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you very much for your work. I followed through your explanation and the code and it makes sense. The implementation is clean, I would not have done things differently.
There is only a consistency nit to address. initialize_runtime() is actually a C++ -> Rust call. we already have a bunch of those but we manage them differently, we assign the function pointers on the Rust side, which now are actually under initialize_runtime() itself. I wonder if all (or most) of those function pointers could be initialized how you did (just expose them from Rust with #[unsafe(no_mangle)] external "C"). So I'd ask if you can do that refactoring also.
Another small nit is the casing of the function, which should follow pascal case: InitializeRuntime

@satherton

Copy link
Copy Markdown

Will there be a new v20 patch release with this fix?

@besauce

besauce commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Thank you very much for your work. I followed through your explanation and the code and it makes sense. The implementation is clean, I would not have done things differently. There is only a consistency nit to address. initialize_runtime() is actually a C++ -> Rust call. we already have a bunch of those but we manage them differently, we assign the function pointers on the Rust side, which now are actually under initialize_runtime() itself. I wonder if all (or most) of those function pointers could be initialized how you did (just expose them from Rust with #[unsafe(no_mangle)] external "C"). So I'd ask if you can do that refactoring also. Another small nit is the casing of the function, which should follow pascal case: InitializeRuntime

Addressing both points and working on it now! I'm going with #[unsafe(export_name = "...")] instead of plain no_mangle so the exported symbols can be PascalCase like the existing C++ names while the Rust fns stay snake_case.

@besauce
besauce force-pushed the steam-probe-unload-crash branch from 6696af4 to 555ed3c Compare July 6, 2026 23:58
@besauce

besauce commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Done and pushed. All the function pointers are now direct exports, the assignments and C++ pointer definitions are gone, and no call sites needed changes. Side effect: the exports are valid from DLL load, so the old null-pointer-before-init hazard is gone. Compiles on master, and I runtime tested the same changes backported to my v20.14.1 build with a full SteamVR session + vrchat

@besauce
besauce force-pushed the steam-probe-unload-crash branch 3 times, most recently from 04c1a96 to cca47c6 Compare July 7, 2026 02:23

@zmerp zmerp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect! thank you :) I'll wait one night (in case if something pops up) then I'll merge

@zmerp

zmerp commented Jul 7, 2026

Copy link
Copy Markdown
Member

@satherton Probably not, but you can use nightly

@zmerp
zmerp added this pull request to the merge queue Jul 8, 2026
Merged via the queue into alvr-org:master with commit e9b8e3a Jul 8, 2026
12 checks passed
@besauce
besauce deleted the steam-probe-unload-crash branch July 8, 2026 17:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants